Chapter 3 Variables and Mutability in Rust
๐ Variables
Variables are immutable by default
By default variables are immutable, to change this we will use mut in front of the definition
let mut number = 5;
Differences between constants and variables
- You are not allowed to use
mutin constants - You declare constants using
constkeyword instead oflet - In constants we always must annotate the type of the constant
- Constants can be declared in any scope of the code
To declare:
const THREE_HOURS_IN_SECONDS: u32 = 60 * 60 * 3;
Shadowing
This concept exists (I think) in all programming world, but in Rust is exciting. When you want to shadow a variable, for example apples, you can overwrite that value using the same name that the original variable:
let apples = 5
let apples = apples + 3 // This will use apples = 5 + 3
Also it works inside of scopes
let apples = 5 // = 5
{
let apples = apples * 2 // = 10
}
let apples = apples + 3 // = 8
This is not the same that use mut, in this case you are really using a new variable, assigning the value of the previous one
Changing the type by shadowing
You can change the type of a variable thanks to shadowing
let spaces = " "; // = " "
let spaces = spaces.len(); // = 4
Shadowing spares us to have to write a new variable with different name, like spaces_str.
๐ท๏ธ Datatypes
Rust is a statically typed language:
A statically typed programming language is one in which variables and data types are checked at compile time, that is, before the program runs. This means that errors are detected early on, which can help prevent common problems during code execution (Tecnobits, 2023)
let guess: u32 = "42".parse().expect("Not a number!");
// unsigned 32-bit number
Rust can show errors many times if we do not specify the type
Rust has 4 primary scalar types of variables
- Integers
- Floating
- characters
- boolean
Integer Types
| Length | Signed | Unsigned |
|---|---|---|
| 8-bit | i8 |
u8 |
| 16-bit | i16 |
u16 |
| 32-bit | i32 |
u32 |
| 64-bit | i64 |
u64 |
| 128-bit | i128 |
u128 |
| Architecture-dependent | isize |
usize |
Integers can be unsigned or signed
The way that you can know how many numbers can you store inside of one of those is using the next equation
Here
Also the
Type of number literals that you can write
| Number literals | Example |
|---|---|
| Decimal | 98_222 |
| Hex | 0xff |
| Octal | 0o77 |
| Binary | 0b1111_0000 |
Byte (u8 only) |
b'A' |
You also can use underscore to separate numbers that could be hard to read, for example, instead of use 10000 you can write 10_000.
Default integer set by Rust is i32
Floating-point type
These are numbers with decimal point. There are with 32-bit and 64-bit, f32 and f64, respectively. Default is f64. This is always signed.
Boolean type
You can specify if you are using boolean specifying bool:
let c: bool = false;
Character type
This is the most primitive alphabetic type
fn main() {
let c = 'z';
let z: char = 'โค'; // with explicit type annotation
let heart_eyed_cat = '๐ป';
}
You must use ' instead of ", which represents string values
Compound types
Rust has two types of this kind. Tuples and arrays
Tuples
They can have a different types of values inside:
fn main() {
let tup: (i32, f64, u8) = (500, 6.4, 1);
}
Destructuring
Also you can define values using each value of a tuple. This is called destructuring.
fn main() {
let tup = (500, 6.4, 1);
let (x, y, z) = tup;
println!("The value of y is: {y}");
}
Access to an specific element in a tuple
Also we can access to an specific element in a tuple using . followed by the index of that element:
fn main() {
let x: (i32, f64, u8) = (500, 6.4, 1);
let five_hundred = x.0;
let six_point_four = x.1;
let one = x.2;
}
Arrays
Unlike a tuple, here we must have the same data type in the whole array:
fn main() {
let a = [1, 2, 3, 4, 5];
}
Arrays can be changed or modified in length, so they are useful if you need an specific number of elements available to use, for example, months of year:
let months = ["January", "February", "March", "April", "May", "June", "July",
"August", "September", "October", "November", "December"];
Also you can specify the type and the amount of elements inside of the array:
let a: [i32; 5] = [1, 2, 3, 4, 5];
// each element is a signed 32-bit number
Similarly, you can set a specified number of elements with the same value inside of an array using a simple expression. For example:
let a = [3; 5];
// It is the same like: let a = [3, 3, 3, 3, 3]
Access to an specific element in an array
fn main() {
let a = [1, 2, 3, 4, 5];
let first = a[0];
let second = a[1];
}
Rust protects you with panicks
In resume, when you try to access past to the end of an array, Rust will show something like this:
thread 'main' panicked at src/main.rs:19:19:
index out of bounds: the len is 5 but the index is 10
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
In other programming languages this happens without checking the error, and the error access to an invalid memory.
๐ฎ Functions
The keyword to declare functions in rust is fn. The way that you can call a function is in this way:
fn main() {
println!("Hello, world!");
another_function();
}
fn another_function() {
println!("Another function.");
}
Parameters
We can define functions with parameters
We can define functions to have parameters, which are special variables that are part of a functionโs signature. When a function has parameters, you can provide it with concrete values for those parameters. Technically, the concrete values are called arguments, but in casual conversation, people tend to use the words parameter and argument interchangeably for either the variables in a functionโs definition or the concrete values passed in when you call a function.
fn main() {
another_function(5);
}
fn another_function(x: i32) {
println!("The value of x is: {x}");
}
It is important to define the type of data that you want in parameters of a function
fn main() {
print_labeled_measurement(5, 'h');
}
fn print_labeled_measurement(value: i32, unit_label: char) {
println!("The measurement is: {value}{unit_label}");
}
Statements and expressions
- statements: Instructions that do something but do not return anything
- expressions: Returns a result
Statements
For example, this is a statement:
fn main() {
let y = 6;
}
Also, for example, you cannot do this:
fn main() {
// This is wrong โ
let x = (let y = 6);
}
In other languages like C, you can do x = y = 6, but here, in rust, you cannot do that. This is why the statement let y = 6 DOES NOT return anything
Expressions
For example, an expression returns a value, calling a function is an expression, for example, a macro from Rust is an expression. A scope created with curly brackets is an expression:
fn main() {
let y = {
let x = 3;
// This is an expression
x + 1
};
println!("The value of y is: {y}");
}
This RETURNS a value. Also you will notice that expression do not use semicolons:
If you add a semicolon to the end of an expression, you turn it into a statement, and it will then not return a value
Function with return values
The way that you can return a value is very simple, first, you need to specify the data type to return in a function using ->:
fn five() -> i32 {
5
}
fn main() {
let x = five();
println!("The value of x is: {x}");
}
As you can see, you can return values without using the keyword return, you just have to avoid the use of semicolon, as you saw in the previous point.
Another example to understand the return values is this:
fn main() {
let x = plus_one(5);
println!("The value of x is: {x}");
}
fn plus_one(x: i32) -> i32 {
x + 1
}
Note that we are not using semicolon, if we put a semicolon at the end of the expression x+1, this will turned in an statement, and this will throw an error.
๐ Control Flow (Conditions and bucles)
If expression
This expression should be familiar to you, it exists in all programming languages and flow structures. In rust conditions if-else are some like this:
fn main() {
if number > 5 {
println!("Number {number} greater than 5");
} else {
println!("Number {number} smaller than 5");
}
}
Each code block using curly brackets in an if condition are called arms, like match expression. else expression is optional same like other programming languages
You cannot do something like this:
fn main() {
let number = 3;
if number {
println!("number was three");
}
}
This is because in Rust, you must be explicit in each condition that you make. In this case this must be a bool type.
Multiple conditions
fn main() {
let number : i8 = 7;
if number < 0 {
println!("Number is negative");
} else if number % 2 != 0 {
println!("Number {number} is an odd number");
} else if number == 0 {
println!("Number is zero ");
} else {
println!("Number {number} is an even number");
}
}
Conditions in let statement
fn main() {
let condition = true;
let number = if condition { 5 } else { 6 };
println!("The value of number is: {number}");
}
The values in this type of if statements must be the same data type. This could be a common error:
fn main() {
let condition = true;
let number = if condition { 5 } else { "six" };
println!("The value of number is: {number}");
}
Loops
There are three type of loops:
loopwhilefor
loop
This will create an infinite loop:
fn main() {
loop {
println!("again!");
}
}
You can interrupt using ctrl+C or using a break statement inside of this. You can combine loops with other control flow structures like conditions or other loop structures.
Return Values with loop
fn main() {
let mut counter = 0;
let result = loop {
counter += 1;
if counter == 10 {
break counter * 2;
}
};
println!("The result is {result}");
}
Note that after the break expression we passed the value returning to our result variable
Labels for loops
This is a way that you can name a loop to avoid ambiguities, for example, if you need to break one of two loops. See the next sample:
fn loops_example_second() {
let mut count = 0;
'counting_up: loop {
println!("count = {count}");
let mut remaining = 10;
loop {
println!("remaining = {remaining}");
if remaining == 9 {
break;
}
if count == 2 {
break 'counting_up; // Here we are specifing that if the first counter is 2, it will break the first loop and all loops inside
}
remaining -= 1;
}
count += 1;
}
println!("End count = {count}");
}
While Bucle
In rust you can make a while bucle using only loop and else-if. Something like this:
fn main() {
let mut number = 3;
// first bucle with loop
loop {
println!("{number} :D");
if number == 1 {
break;
}
number -= 1;
}
println!("HAPPY BIRTHDAY");
}
But in Rust already exists a bucle specialized in this kind of bucles, this is the while bucle:
fn main() {
let mut number = 3;
while number != 0 {
println!("{number}!");
number -= 1;
}
println!("LIFTOFF!!!");
}
Looping in a collection with for
You can loop a collection using a while bucle like this:
fn main() {
let a = [10, 20, 30, 40, 50];
let mut index = 0;
while index < 5 {
println!("the value is: {}", a[index]);
index += 1;
}
}
However this way to loop a collection is an error-prone and also is slower because the program each time that pass an element must evaluate the condition to stop the program. Fortunately, rust also has a loop known as for, in other programming languages is better known as foreach, and this kind of loop is better to loop a collection:
fn main() {
let a = [10, 20, 30, 40, 50];
for element in a {
println!("The element is: {element}");
}
}
Machine code generated from for loops can be more efficient as well because the index doesnโt need to be compared to the length of the array at every iteration.
For loop is one of the most used loops used in Rust
Countdown using for bucle:
fn main() {
for counter in (1..4).rev() {
println!("counter: {counter}");
}
}